<!doctype html>
<!-- Based on layout/style/test/test_transitions.html in Gecko -->
<title>Transitions tests</title>
<script src=http://w3c-test.org/resources/testharness.js></script>
<script src=http://w3c-test.org/resources/testharnessreport.js></script>
<script src="animation_utils.js"></script>
<style>
#tests { line-height: 0.8; font-size: 0.8em }
#tests p { margin: 0 }
/* Needed for pseudo-element tests */
#tests .before, #tests .after {
	/* display: table for shrink-to-fit */
	display: table;
	border: 1px solid black;
}
#tests .before::before, #tests .after::after {
	display: block;
	width: 0;
	text-indent: 0;
}
#tests .before.started::before, #tests .after.started::after {
	width: 100px;
	text-indent: 100px;
	-ms-transition: 8s width ease-in-out, 8s text-indent ease-in-out;
	-moz-transition: 8s width ease-in-out, 8s text-indent ease-in-out;
	-webkit-transition: 8s width ease-in-out, 8s text-indent ease-in-out;
	-o-transition: 8s width ease-in-out, 8s text-indent ease-in-out;
	transition: 8s width ease-in-out, 8s text-indent ease-in-out;
}
#tests .before::before {
	content: "Before";
}
#tests .after::after {
	content: "After";
}
</style>
<div id=tests></div>
<div id=log></div>
<script>
"use strict";

var prop = "transition" in document.body.style ? "transition"
	: "msTransition" in document.body.style ? "msTransition"
	: "MozTransition" in document.body.style ? "MozTransition"
	: "webkitTransition" in document.body.style ? "webkitTransition"
	: "OTransition" in document.body.style ? "OTransition"
	: undefined;

var hyphPrefix = {
	transition: "",
	msTransition: "-ms-",
	MozTransition: "-moz-",
	webkitTransition: "-webkit-",
	OTransition: "-o-",
}[prop];

if (prop === undefined) {
	throw "Transitions not supported?";
}

setup({timeout: 9000});

add_completion_callback(function() {
	document.body.removeChild(document.querySelector("#tests"));
});

/**
 * Assert that a transition whose timing function yields the bezier
 * |func|, running from |startTime| to |endTime| (both in seconds
 * relative to when the transitions were started) should have produced
 * computed value |computedValue| given that the transition was from
 * |startValue| to |endValue| (both numbers in CSS pixels).
 */
function checkTransitionValue(func, startTime, endTime,
							  startValue, endValue, computedValue, t)
{
	var valueAt = function(elapsed, errorDirection) {
		var timePortion = (elapsed - startTime) / (endTime - startTime);
		if (timePortion < 0) {
			timePortion = 0;
		} else if (timePortion > 1) {
			timePortion = 1;
		}
		// Assume a small error since bezier computation can be off slightly.
		var valuePortion = func(timePortion) + errorDirection * 0.0015;
		if (valuePortion < 0) {
			valuePortion = 0;
		} else if (valuePortion > 1) {
			valuePortion = 1;
		}
		var value = (1 - valuePortion) * startValue + valuePortion * endValue;
		if (startValue > endValue) {
			errorDirection = -errorDirection;
		}
		// TODO: Should this be more precise?  Gecko has * 0.02 . . .
		return value + errorDirection * 1.5;
	}

	var timeRange; // in seconds
	var unsortedRange; // |range| before being sorted (so errors give it
				   // in the original order
	if (!gSetupComplete) {
		// No timers involved
		timeRange = [0, 0];
		if (startTime < 0) {
			unsortedRange = [valueAt(0, -1), valueAt(0, 1)];
		} else {
			var val = valueAt(0, 0);
			unsortedRange = [val, val];
		}
	} else {
		timeRange = [px_to_num(tests.earlyRef.computed.textIndent)/125,
					 px_to_num(tests.lateRef.computed.textIndent)/125];
		// seconds
		unsortedRange = [valueAt(timeRange[0], -1),
					     valueAt(timeRange[1], 1)];
	}
	var range = unsortedRange.concat() // concat to clone array
				.sort(function(a,b) { return a - b });
	var actual = px_to_num(computedValue);

	t.step(function() {
		assert_approx_equals(actual,
			(range[0] + range[1])/2,
			(range[1] - range[0])/2,
			"computed value " + computedValue + " should be between " +
			unsortedRange[0].toFixed(6) + "px and " + unsortedRange[1].toFixed(6) +
			"px at time between " + timeRange[0] + "s and " + timeRange[1] + "s.");
	});
}

// An array of arrays of functions to be called at the outer index number
// of seconds after the present.
(function() {
var futureCalls = [];

window.addFutureCall = function(index, func) {
	if (!(index in futureCalls)) {
		futureCalls[index] = [];
	}
	futureCalls[index].push(func);
}

window.processFutureCalls = function(index) {
	var calls = futureCalls[index];
	if (!calls) {
		return;
	}
	gCurrentTime = Date.now();
	calls.forEach(function(call) { call() });
	if (index == 8) {
		testObjects.forEach(function(t) {
			t.done();
		});
	}
}
})();
var gStartTime1, gStartTime2;
var gCurrentTime;
var gSetupComplete = false;

var timingFunctions = {
  // a map from the value of 'transition-timing-function' to an array of
  // the portions this function yields at 0 (always 0), 1/4, 1/2, and
  // 3/4 and all (always 1) of the way through the time of the
  // transition.  Each portion is represented as a value and an
  // acceptable error tolerance (based on a time error of 1%) for that
  // value.

  // ease
  "ease": bezier(0.25, 0.1, 0.25, 1),
  "cubic-bezier(0.25, 0.1, 0.25, 1.0)": bezier(0.25, 0.1, 0.25, 1),

  // linear and various synonyms for it
  "linear": function(x) { return x; },
  "cubic-bezier(0.0, 0.0, 1.0, 1.0)": function(x) { return x; },
  "cubic-bezier(0, 0, 1, 1)": function(x) { return x; },
  "cubic-bezier(0, 0, 0, 0.0)": function(x) { return x; },
  "cubic-bezier(1.0, 1, 0, 0)": function(x) { return x; },

  // ease-in
  "ease-in": bezier(0.42, 0, 1, 1),
  "cubic-bezier(0.42, 0, 1.0, 1.0)": bezier(0.42, 0, 1, 1),

  // ease-out
  "ease-out": bezier(0, 0, 0.58, 1),
  "cubic-bezier(0, 0, 0.58, 1.0)": bezier(0, 0, 0.58, 1),

  // ease-in-out
  "ease-in-out": bezier(0.42, 0, 0.58, 1),
  "cubic-bezier(0.42, 0, 0.58, 1.0)": bezier(0.42, 0, 0.58, 1),

  // other cubic-bezier values
  "cubic-bezier(0.4, 0.1, 0.7, 0.95)": bezier(0.4, 0.1, 0.7, 0.95),
  "cubic-bezier(1, 0, 0, 1)": bezier(1, 0, 0, 1),
  "cubic-bezier(0, 1, 1, 0)": bezier(0, 1, 1, 0),
};

var div = document.getElementById("tests");

// Test objects, which will have done() called on all of them after the last
// timers fire
var testObjects = [];

// Test types.  Each one has the following methods:
//   setup: Creates the elements and gives them the right properties, but
//     doesn't start the transitions yet
//   start: Starts the transitions
//   check: Runs appropriate addFutureCall()s to run step() on the test objects
var tests = {
	// We have two reference elements to check the expected timing range.
	// They both have 8s linear transitions from 0 to 1000px.
	earlyRef: {
		setup: function() {
			this.para = makeReferenceParagraph();
			this.computed = getComputedStyle(this.para);
			this.testObj = async_test("Early reference", {timeout: 9000});
			testObjects.push(this.testObj);
		},
		start: function() {
			this.para.style.textIndent = "1000px";
		},
		check: function() {
			// Handles both early and late ref
			for (var i = 1; i <= 8; ++i) {
				addFutureCall(i, function() {
					// This is the only test where we compare the progress of the
					// transitions to an actual time; we need considerable tolerance at
					// the low end (we are using half a second).
					var expectedRange = [(gCurrentTime - gStartTime2 - 40)/8,
										 (Date.now() - gStartTime1 + 20)/8];
					if (expectedRange[0] > 1000) {
						expectedRange[0] = 1000;
					}
					if (expectedRange[1] > 1000) {
						expectedRange[1] = 1000;
					}
					var check = function(t, value) {
						t.step(function() {
							assert_approx_equals(value,
								(expectedRange[0] + expectedRange[1])/2,
								(expectedRange[1] - expectedRange[0])/2,
								"computed value " + value + "px should be between " +
								expectedRange[0].toFixed(6) + "px and " +
								expectedRange[1].toFixed(6) + "px at time between " +
								expectedRange[0]/125 + "s and " + expectedRange[1]/125 + "s.");
						});
					}
					check(this.testObj, px_to_num(this.computed.textIndent));
					check(tests.lateRef.testObj, px_to_num(tests.lateRef.computed.textIndent));
				}.bind(this));
			}
		},
	},

	// Test all timing functions using a set of 8-second transitions, which we
	// check at times 0, 2s, 4s, 6s, and 8s.
	timingFunction: {
		setup: function() {
			this.tests = [];
			for (var tf in timingFunctions) {
				var p = document.createElement("p");
				p.appendChild(document.createTextNode("transition-timing-function: " + tf));
				p.style.textIndent = "0px";
				p.style[prop] = "8s text-indent " + tf;
				div.appendChild(p);
				var t = async_test(p.textContent, {timeout: 9000});
				t.step(function() {
					assert_equals(getComputedStyle(p).textIndent, "0px",
						"text-indent must be zero before changing value");
				});
				this.tests.push([p, tf, t]);
				testObjects.push(t);
			}
		},
		start: function() {
			this.tests.forEach(function(arr) {
				arr[0].style.textIndent = "100px";
			});
		},
		check: function() {
			var check = function() {
				this.tests.forEach(function(arr) {
					var p = arr[0];
					var tf = arr[1];
					var t = arr[2];

					checkTransitionValue(timingFunctions[tf], 0, 8, 0, 100,
						getComputedStyle(p).textIndent, t);

				});

				tests.interrupt.doCheck();
			}.bind(this);

			check();
			addFutureCall(2, check);
			addFutureCall(4, check);
			addFutureCall(6, check);
			addFutureCall(8, check);
		},
	},

	// Check that the timing function continues even when we restyle in the
	// middle.
	interrupt: {
		setup: function() {
			this.tests = [];
			[true, false].forEach(function(restyleParent) {
				for (var itime = 2; itime < 8; itime += 2) {
					var p = document.createElement("p");
					p.appendChild(document.createTextNode("interrupt on " +
						(restyleParent ? "parent" : "node itself") +
						" at " + itime + "s"));
					p.style.textIndent = "0px";
					p.style[prop] = "8s text-indent cubic-bezier(0, 1, 1, 0)";
					if (restyleParent) {
						var d = document.createElement("div");
						d.appendChild(p);
						div.appendChild(d);
					} else {
						div.appendChild(p);
					}
					var t = async_test(p.textContent, {timeout: 9000});
					t.step(function() {
						assert_equals(getComputedStyle(p).textIndent, "0px",
							"text-indent must be zero before changing value");
					});
					(function(el) {
						setTimeout(function() {
							el.style.color = 'blue';
							this.doCheck();
						}.bind(this), itime*1000);
					}).bind(this)(restyleParent ? p.parentNode : p);
					this.tests.push([p, itime, t]);
					testObjects.push(t);
				}
			}.bind(this));
		},
		// This is called from tests.timingFunction.check and where we reset the
		// interrupts
		doCheck: function() {
			this.tests.forEach(function(arr) {
				var p = arr[0];
				var itime = arr[1];
				var t = arr[2];

				checkTransitionValue(timingFunctions["cubic-bezier(0, 1, 1, 0)"],
					0, 8, 0, 100,
					getComputedStyle(p).textIndent,
					t);
		   });
		},
		start: function() {
			this.tests.forEach(function(arr) {
				arr[0].style.textIndent = "100px";
			});
		},
		check: function() {},
	},

	// Test transition-delay values of -4s through 4s on a 4s transition
	// with 'ease-out' timing function.
	delay: {
		// Called by both tests.delay.setup and tests.delayZero.setup
		sharedSetup: function(value, desc) {
			this.tests = {};
			for (var d = -4; d <= 4; d++) {
				var p = document.createElement("p");
				var delay = d + "s";
				p.appendChild(document.createTextNode(desc
					+ "transition-delay: " + delay));
				p.style.marginLeft = "0px";
				p.style[prop] = value + delay;
				div.appendChild(p);
				var t = async_test(p.textContent, {timeout: 9000});
				t.step(function() {
					assert_equals(getComputedStyle(p).marginLeft, "0px",
						"margin-left must be zero before changing value");
				});
				this.tests[d] = [p, t];
				testObjects.push(t);
			}
		},
		setup: function() {
			this.sharedSetup("4s margin-left ease-out ",
				"transition-duration: 4s; ");
		},
		start: function() {
			for (var d in this.tests) {
				this.tests[d][0].style.marginLeft = "100px";
			}
		},
		check: function() {
			var check = function(time) {
				var tf = timingFunctions["ease-out"];
				for (var d in this.tests) {
					var p = this.tests[d][0];
					var t = this.tests[d][1];

					if (d >= 0 || time > 0) {
						// FIXME: Browsers don't agree if d < 0 and time == 0
						// https://www.w3.org/Bugs/Public/show_bug.cgi?id=16016#c3
						checkTransitionValue(tf, Number(d), Number(d) + 4, 0, 100,
							getComputedStyle(p).marginLeft, t);
					}
				}
			}.bind(this);

			check(0);
			for (var i = 1; i <= 8; i++) {
				addFutureCall(i, check);
			}
		},
	},

	// Test transition-delay values of -4s through 4s on a 4s transition with
	// duration of zero.
	delayZero: {
		setup: function() {
			tests.delay.sharedSetup.bind(this)("0s margin-left linear ",
				"transition-duration: 0s; ");
		},
		start: function() {
			for (var d in this.tests) {
				this.tests[d][0].style.marginLeft = "100px";
			}
		},
		check: function() {
			var check = function(time) {
				for (var d in this.tests) {
					var p = this.tests[d][0];
					var t = this.tests[d][1];

					var timeRange = [px_to_num(tests.earlyRef.computed.textIndent)/125,
					                 px_to_num(tests.lateRef.computed.textIndent)/125];
					var m = getComputedStyle(p).marginLeft;
					var desc = "margin-left between " + timeRange[0] + "s and "
						+ timeRange[1] + "s";
					t.step(function() {
						if (d <= 0 && time == 0) {
							// FIXME: Browsers don't agree
							// https://www.w3.org/Bugs/Public/show_bug.cgi?id=16016#c3
							return;
						}
						if (timeRange[0] < d && timeRange[1] < d) {
							assert_equals(m, "0px", desc);
						} else if ((timeRange[0] > d && timeRange[1] > d) ||
						(d == 0 && time == 0)) {
							assert_equals(m, "100px", desc);
						}
					});
				}
			}.bind(this);

			check(0);
			for (var i = 1; i <= 8; ++i) {
				addFutureCall(i, check);
			}
		},
	},

	// Test that changing the value on an already-running transition to the
	// value it currently happens to have resets the transition.
	reset: {
		setup: function() {
			this.testObj = async_test("transition-delay reset to starting point",
				{timeout: 9000});
			testObjects.push(this.testObj);

			var make = function(transition, description) {
				var p = document.createElement("p");
				p.appendChild(document.createTextNode(description));
				p.style.marginLeft = "0px";
				p.style[prop] = transition;
				div.appendChild(p);
				this.testObj.step(function() {
					assert_equals(getComputedStyle(p).marginLeft, "0px",
						"margin-left must be zero before changing value");
				});
				return p;
			}.bind(this);
			this.para = make("4s margin-left ease-out 4s",
				"transition-delay reset to starting point");
			this.ref = make("4s margin-left linear -3s",
				"reference for previous test (reset test)");
		},
		start: function() {
			this.para.style.marginLeft = "100px";
			this.ref.style.marginLeft = "100px";
		},
		check: function() {
			var check = function(time) {
				this.testObj.step(function() {
					assert_equals(getComputedStyle(this.para).marginLeft, "0px",
						"reset test value at time " + time + "s.");
				}.bind(this));
			}.bind(this);

			// FIXME: Browsers don't agree at t = 0, so comment those out
			// https://www.w3.org/Bugs/Public/show_bug.cgi?id=16016#c3
			//check(0);
			// Reset the reset test right now so we don't have to worry about
			// clock skew.  To make sure that this is valid, check that a
			// pretty-much-identical test is already transitioning.
			/*
			this.testObj.step(function() {
				assert_equals(getComputedStyle(this.ref).marginLeft, "75px",
					"reset test reference value");
			}.bind(this));
			*/
			this.para.style.marginLeft = "0px";
			//check(0);
			for (var i = 1; i <= 8; ++i) {
				addFutureCall(i, function(j) { check(j) });
			}
		},
	},

	descendant: {
		setup: function() {
			// Test that transitions on descendants do not trigger when the
			// inherited value is itself transitioning.  In other words, when
			// ancestor and descendant both have a transition for the same
			// property, and the descendant inherits the property from the
			// ancestor, the descendant's transition is ignored (as part of the
			// idea of not starting transitions on changes that result from
			// animation).
			// See http://lists.w3.org/Archives/Public/www-style/2009Jun/0121.html
			// and http://lists.w3.org/Archives/Public/www-style/2009Jul/0050.html
			this.tests = [
				{ parent: "", child: "4s linear text-indent" },
				{ parent: "4s linear text-indent", child: "" },
				{ parent: "4s linear text-indent", child: "16s linear text-indent" },
				{ parent: "4s linear text-indent", child: "1s linear text-indent" },
				{ parent: "8s linear letter-spacing", child: "4s linear text-indent" },
				{ parent: "4s linear text-indent", child: "8s linear letter-spacing" },
				{ parent: "4s linear text-indent", child: "8s linear all" },
				{ parent: "8s linear text-indent", child: "4s linear all" },
				// examples linear with positive and negative delay
				{ parent: "4s linear text-indent 1s", child: "8s linear text-indent" },
				{ parent: "4s linear text-indent -1s", child: "8s linear text-indent" }
			];

			this.tests.forEach(function(test) {
				test.parentNode = document.createElement("div");
				test.childNode = document.createElement("p");
				test.parentNode.appendChild(test.childNode);
				test.childNode.appendChild(document.createTextNode(
					'parent with "' + test.parent + '" and ' +
					'child with "' + test.child + '"'));
				test.parentNode.style[prop] = test.parent;
				test.childNode.style[prop] = test.child;
				test.parentNode.style.textIndent = "50px"; // transition from 50 to 150
				test.parentNode.style.letterSpacing = "10px"; // transition from 10 to 5
				div.appendChild(test.parentNode);

				test.textIndentTestObj = async_test(test.childNode.textContent
					+ " (text-indent tests)", {timeout: 9000});
				testObjects.push(test.textIndentTestObj);
				test.letterSpacingTestObj = async_test(test.childNode.textContent
					+ " (letter-spacing tests)", {timeout: 9000});
				testObjects.push(test.letterSpacingTestObj);

				var parentComputed = getComputedStyle(test.parentNode);
				var childComputed = getComputedStyle(test.childNode);
				test.textIndentTestObj.step(function() {
					assert_equals(parentComputed.textIndent, "50px",
						"parent text-indent should be 50px before changing");
					assert_equals(childComputed.textIndent, "50px",
						"child text-indent should be 50px before changing");
				});
				test.letterSpacingTestObj.step(function() {
					assert_equals(parentComputed.letterSpacing, "10px",
						"parent letter-spacing should be 10px before changing");
					assert_equals(childComputed.letterSpacing, "10px",
						"child letter-spacing should be 10px before changing");
				});
				test.childComputed = childComputed;
			}.bind(this));
		},
		start: function() {
			this.tests.forEach(function(test) {
				test.parentNode.style.textIndent = "150px";
				test.parentNode.style.letterSpacing = "5px";
			});
		},
		check: function() {
			var check = function() {
				var tf = timingFunctions["linear"];

				this.tests.forEach(function(test) {
					var parentStyle = getComputedStyle(test.parentNode);
					var childStyle = getComputedStyle(test.childNode);

					var indentDuration, indentDelay;
					var spacingDuration, spacingDelay;
					if (parentStyle[prop + "Property"] == "text-indent") {
						indentDuration = parseFloat(parentStyle[prop + "Duration"]);
						indentDelay = parseFloat(parentStyle[prop + "Delay"]);
					} else if (childStyle[prop + "Property"] != "letter-spacing") {
						indentDuration = parseFloat(childStyle[prop + "Duration"]);
						indentDelay = parseFloat(childStyle[prop + "Delay"]);
					}
					if (indentDuration === undefined || isNaN(indentDuration)) {
						indentDuration = 0.01;
					}
					if (indentDelay === undefined || isNaN(indentDelay)) {
						indentDelay = -1;
					}

					if (parentStyle[prop + "Property"] == "letter-spacing") {
						spacingDuration = parseFloat(parentStyle[prop + "Duration"]);
						spacingDelay = parseFloat(parentStyle[prop + "Delay"]);
					} else if (childStyle[prop + "Property"] != "text-indent") {
						spacingDuration = parseFloat(childStyle[prop + "Duration"]);
						spacingDelay = parseFloat(childStyle[prop + "Delay"]);
					}
					if (spacingDuration === undefined || isNaN(spacingDuration)) {
						spacingDuration = 0.01;
					}
					if (spacingDelay === undefined || isNaN(spacingDelay)) {
						spacingDelay = -1;
					}

					checkTransitionValue(tf, indentDelay,
						indentDelay + indentDuration, 50, 150,
						test.childComputed.getPropertyValue("text-indent"),
						test.textIndentTestObj);

					checkTransitionValue(tf, spacingDelay,
						spacingDelay + spacingDuration, 10, 5,
						test.childComputed.getPropertyValue("letter-spacing"),
						test.letterSpacingTestObj);
				});
			}.bind(this);

			// FIXME: Browsers don't agree at t = 0, so comment this out
			// https://www.w3.org/Bugs/Public/show_bug.cgi?id=16016#c3
			//check();
			addFutureCall(2, check);
			addFutureCall(6, check);
		},
	},

	// For all of these transitions, the transition for margin-left should have
	// a duration of 8s, and the default timing function (ease) and delay (0).
	number: {
		setup: function() {
			this.tests = [
				{ style: "transition: 4s margin, 8s margin-left" },
				{ style: "transition: 4s margin-left, 8s margin" },
				{ style: "transition-property: margin-left; " +
					"transition-duration: 8s, 2s" },
				{ style: "transition-property: margin-left, margin-left; " +
					"transition-duration: 2s, 8s" },
				{ style: "transition-property: margin-left, margin-left, margin-left; " +
					"transition-duration: 8s, 2s" },
				// FIXME: This test's expected behavior is not yet mandated:
				// https://www.w3.org/Bugs/Public/show_bug.cgi?id=14604
				{ style: "transition-property: margin-left; " +
					"transition-duration: 8s, 16s" },
				{ style: "transition-property: margin-left, margin-left; " +
					"transition-duration: 16s, 8s" },
				{ style: "transition-property: margin-left, margin-left, margin-left; " +
					"transition-duration: 8s, 16s" },
				{ style: "transition-property: text-indent,word-spacing,margin-left; " +
					"transition-duration: 8s; " +
					"transition-delay: 0, 8s" },
				// FIXME: This test's expected behavior is not yet mandated:
				// https://www.w3.org/Bugs/Public/show_bug.cgi?id=14604
				{ style: "transition-property: text-indent,word-spacing,margin-left; " +
					"transition-duration: 8s, 16s; " +
					"transition-delay: 8s, 8s, 0, 8s, 8s, 8s" },
			];

			this.tests.forEach(function(test) {
				var p = document.createElement("p");
				// Make failures easier to understand by using linear timing
				test.style += "; transition-timing-function: linear";
				p.setAttribute("style", test.style
					.replace(/transition/g, hyphPrefix + "transition"));
				p.appendChild(document.createTextNode(test.style));
				p.style.marginLeft = "100px";
				div.appendChild(p);
				var t = async_test(p.textContent, {timeout: 9000});
				t.step(function() {
					assert_equals(getComputedStyle(p).marginLeft, "100px",
						"margin-left must be 100px before changing value");
				});
				test.node = p;
				test.testObj = t;
				testObjects.push(t);
			});
		},
		start: function() {
			this.tests.forEach(function(test) {
				test.node.style.marginLeft = "200px";
			});
		},
		check: function() {
			var check = function() {
				var tf = timingFunctions.linear;
				this.tests.forEach(function(test) {
					checkTransitionValue(tf, 0, 8, 100, 200,
						getComputedStyle(test.node).marginLeft,
						test.testObj);
				});
			}.bind(this);

			check(0);
			addFutureCall(2, check);
			addFutureCall(4, check);
			addFutureCall(6, check);
			addFutureCall(8, check);
		},
	},

	// Test transitions that are also from-display:none, to-display:none, and
	// display:none throughout.
	display: {
		setup: function() {
			function make(initiallyNone, text) {
				var p = document.createElement("p");
				p.appendChild(document.createTextNode(text));
				p.style.textIndent = "0px";
				p.style[prop] = "8s text-indent ease-in-out";
				if (initiallyNone) {
					p.style.display = "none";
				}
				div.appendChild(p);

				var t = async_test(text, {timeout: 9000});
				testObjects.push(t);

				return [p, t];
			}
			this.fromNoneTest   = make(true,  "transition from display:none");
			this.toNoneTest     = make(false, "transition to display:none");
			this.alwaysNoneTest = make(true,  "transition always display:none");
			this.tests = [this.fromNoneTest, this.toNoneTest, this.alwaysNoneTest];
		},
		start: function() {
			this.fromNoneTest[0].style.textIndent = "100px";
			this.fromNoneTest[0].style.display = "";
			this.toNoneTest[0].style.textIndent = "100px";
			this.toNoneTest[0].style.display = "none";
			this.alwaysNoneTest[0].style.textIndent = "100px";
		},
		check: function() {
			var check = function(time) {
				var tf = timingFunctions["ease-in-out"];
				this.tests.forEach(function(arr) {
					var p = arr[0];
					var t = arr[1];

					checkTransitionValue(tf, 0, 8, 0, 100,
						getComputedStyle(p).textIndent, t);
				});
			}.bind(this);

			// FIXME: Browsers don't agree at t = 0, so comment this out
			// https://www.w3.org/Bugs/Public/show_bug.cgi?id=16016#c3
			//check(0);
			addFutureCall(2, function() { check(2); });
			addFutureCall(4, function() { check(4); });
			addFutureCall(6, function() { check(6); });
			addFutureCall(8, function() { check(8); });
		},
	},

	// Test transitions on pseudo-elements
	pseudo: {
		setup: function() {
			function make(pseudo) {
				var p = document.createElement("p");
				p.className = pseudo;
				div.appendChild(p);

				var widthTest = async_test("::" + pseudo + " width test",
					{timeout: 9000});
				var indentTest = async_test("::"+pseudo+" text-indent test",
					{timeout: 9000});
				testObjects.push(widthTest, indentTest);

				return {pseudo: pseudo, element: p,
					widthTest: widthTest, indentTest: indentTest};
			}
			this.tests = [make("before"), make("after")];
		},
		start: function() {
			this.tests.forEach(function(test) {
				test.element.className += " started";
			});
		},
		check: function() {
			var check = function(time) {
				var tf = timingFunctions["ease-in-out"];
				this.tests.forEach(function(test) {
					checkTransitionValue(tf, 0, 8, 0, 100,
						getComputedStyle(test.element).width,
						test.widthTest);
					checkTransitionValue(tf, 0, 8, 0, 100,
						getComputedStyle(test.element, "::" + test.pseudo).textIndent,
						test.indentTest);
				});
			}.bind(this);
			// FIXME: Browsers don't agree at t = 0, so comment this out
			// https://www.w3.org/Bugs/Public/show_bug.cgi?id=16016#c3
			//check(0);
			addFutureCall(2, function() { check(2); });
			addFutureCall(4, function() { check(4); });
			addFutureCall(6, function() { check(6); });
			addFutureCall(8, function() { check(8); });
		},
	},

	lateRef: {
		setup: function() {
			this.para = makeReferenceParagraph();
			this.computed = getComputedStyle(this.para);
			this.testObj = async_test("Late reference", {timeout: 9000});
			testObjects.push(this.testObj);
		},
		start: function() {
			this.para.style.textIndent = "1000px";
		},
		check: function() {
			// Handled by earlyRef
		},
	},
};


// Set up all the elements on which we are going to start transitions.

function makeReferenceParagraph() {
	var p = document.createElement("p");
	p.appendChild(document.createTextNode("reference"));
	p.style.textIndent = "0px";
	p.style[prop] = "8s text-indent linear";
	div.appendChild(p);
	return p;
}

for (var type in tests) {
	tests[type].setup();
}


// flush style changes; FIXME: not officially required to work
// https://www.w3.org/Bugs/Public/show_bug.cgi?id=16016
var x = getComputedStyle(div).color;

// Start our timer as close as possible to when we start the first
// transition.
// Do not use setInterval because once it gets off in time, it stays off.
for (var i = 1; i <= 8; i++) {
	setTimeout(processFutureCalls.bind(null, i), i * 1000);
}
gStartTime1 = Date.now(); // set before any transitions have started


// Start all the transitions.
for (var type in tests) {
	tests[type].start();
}


// flush style changes; FIXME: not officially required to work
// https://www.w3.org/Bugs/Public/show_bug.cgi?id=16016
x = getComputedStyle(div).color;


gStartTime2 = Date.now(); // set after all transitions have started
gCurrentTime = gStartTime2;

for (var type in tests) {
	tests[type].check();
}

gSetupComplete = true;
</script>
